Frontend Forever App
We have a mobile app for you to download and use. And you can unlock many features in the app.
Get it now
Intall Later
Run
HTML
CSS
Javascript
Output
Document
scroll.
@charset "UTF-8"; @import url(https://fonts.googleapis.com/css?family=Nunito+Sans:300,400,600,700,800); *, :after, :before { box-sizing: border-box; padding: 0; margin: 0; } @import "normalize.css"; html { color-scheme: light only; } [data-dark="true"] { color-scheme: dark only; } *, *:after, *:before { box-sizing: border-box; } body { display: grid; place-items: center; min-height: 100svh; font-family: "SF Pro Text", "SF Pro Icons", "AOS Icons", "Helvetica Neue", Helvetica, Arial, sans-serif, system-ui; overflow: hidden; } body::before { --size: 60px; --line: color-mix(in lch, canvasText 35%, hsl(0 0% 100% / 0.25)); content: ''; height: 100svh; width: 100vw; position: fixed; background: linear-gradient( 90deg, var(--line) 1px, transparent 1px var(--size) ) 50% 50% / var(--size) var(--size), linear-gradient(var(--line) 1px, transparent 1px var(--size)) 50% 50% / var(--size) var(--size); mask: linear-gradient(-25deg, transparent 65%, white); top: 0; transform-style: flat; pointer-events: none; z-index: -1; } main { transform-style: preserve-3d; position: relative; transition: scale 0.2s; height: 100svh; display: grid; place-items: center; width: 100%; } .container { --scroll-padding: 0px; --inner-angle: calc((360 / var(--total)) * 1deg); --item-width: clamp(120px, 20vmin, 12rem); --scroll-buff: calc(var(--item-width) * var(--scroll-ratio)); --gap: calc(var(--item-width) * var(--gap-efficient, 0.1)); --radius: calc(((var(--item-width) + var(--gap)) / sin(var(--inner-angle))) * -1); position: relative; width: 100%; height: 100%; overflow: auto; timeline-scope: --controller; } [data-infinite="true"] .container { --scroll-padding: calc(var(--item-width) * 0.01); --scroll-padding: 5px; } ul { list-style-type: none; padding: 0; margin: 0; display: grid; } img { border-radius: 12px; width: 100%; height: 100%; background-color: color-mix(in lch, canvasText 25%, canvas); } h1 { position: fixed; bottom: 1rem; right: 1rem; color: color-mix(in lch, canvasText, transparent); opacity: 0; transition: opacity 0.2s; } [data-scroll=true] h1 { opacity: 1; } .carousel { padding: 0; } .carousel { display: flex; } .carousel { width: 100%; height: 100%; position: absolute; left: 50%; top: 50%; translate: -50% -50%; } .carousel-container { height: 100%; width: 100%; mask: linear-gradient( calc(90deg + (var(--rotate-z) * 1deg)), transparent 0 calc(50% - (var(--item-width) * var(--mask-upper))), white calc(50% - (var(--mask-lower) * var(--item-width))) calc(50% + (var(--mask-lower) * var(--item-width))), transparent calc(50% + (var(--mask-upper) * var(--item-width))) 100% ); pointer-events: none; position: absolute; display: grid; place-items: center; inset: 0; transform-style: preserve-3d; perspective: calc(var(--perspective) * 1px); overflow: hidden; } [data-scroll="true"] .carousel::after, [data-animate="true"] .carousel::after { animation: shine calc(var(--total) * 0.5s) infinite linear; } [data-scroll="true"] .carousel, [data-animate="true"] .carousel { animation: carousel calc(var(--total) * 0.5s) infinite linear; } .carousel { transform-style: preserve-3d; transform: translate3d(0, 0, var(--radius)) rotateX(calc(var(--rotate-x) * 1deg)) rotateZ(calc(var(--rotate-z) * 1deg)) rotateY(0deg); } @keyframes carousel { to { transform: translate3d(0, 0, var(--radius)) rotateX(calc(var(--rotate-x) * 1deg)) rotateZ(calc(var(--rotate-z) * 1deg)) rotateY(-360deg); } } @supports(animation-timeline: scroll()) { [data-scroll="true"] .carousel, [data-scroll="true"] .carousel::after { animation-duration: auto; animation-iteration-count: 1; animation-fill-mode: both; animation-timeline: --controller; } } .carousel::after { content: ""; position: absolute; top: 50%; left: 50%; width: calc(var(--item-width) * 1.5); height: calc(var(--item-width) * 1.5); backdrop-filter: brightness(2); mask: radial-gradient(80% 80% at 50% 50%, white, transparent 50%); transform: translate(-50%, -50%) rotateY(0deg) translate3d(0, 0, calc((var(--radius) - 10px) * -1)); } @keyframes shine { to { transform: translate(-50%, -50%) rotateY(360deg) translate3d(0, 0, calc((var(--radius) - 10px) * -1)); } } [data-gsap="true"] .carousel::after { animation: none; transform: translate(-50%, -50%) rotateY(calc(var(--rotate) * 1deg)) translate3d(0, 0, calc((var(--radius) - 10px) * -1)); } .carousel li { --debug: red; height: var(--item-width); width: var(--item-width); outline-offset: 2px; } .carousel li { position: absolute; top: 50%; left: 50%; backface-visibility: hidden; transform: translate(-50%, -50%) rotateY(calc((var(--inner-angle) * var(--index)))) translate3d(0, 0, calc(var(--radius) * -1)); } [data-backface="true"] .carousel li { backface-visibility: visible; } .controller { display: flex; overflow: auto; width: 100%; height: 100%; scroll-snap-type: x mandatory; scroll-timeline: --controller inline; align-items: center; padding-inline: calc((50vw + var(--scroll-padding)) - (var(--scroll-buff) * 0.5)); } [data-vertical="true"] .controller { flex-direction: column; scroll-snap-type: y mandatory; scroll-timeline: --controller; padding-inline: 0; padding-block: calc((50svh + var(--scroll-padding)) - (var(--scroll-buff) * 0.5)); } .controller li { --debug: red; height: var(--scroll-buff); width: var(--scroll-buff); aspect-ratio: 1; flex: 1 0 auto; scroll-snap-align: center; } [data-debug="true"] li { outline: 2px dashed var(--debug); } /*.carousel { animation: carousel both linear; animation-timeline: --controller; animation-range: var(--scroll-padding) calc(100% - var(--scroll-padding)); }*/ /*.carousel li { filter: saturate(0.75) brightness(0.75); }*/ div.tp-lblv_v { flex-shrink: 1; width: auto; } div.tp-dfwv { width: 300px; } .bear-link { color: canvasText; position: fixed; bottom: 1rem; left: 1rem; width: 48px; aspect-ratio: 1; display: grid; place-items: center; opacity: 0.8; } :where(.x-link, .bear-link):is(:hover, :focus-visible) { opacity: 1; } .bear-link svg { width: 75%; }
console.log("Event Fired") import { Pane } from 'https://cdn.skypack.dev/tweakpane' import gsap from 'https://cdn.skypack.dev/gsap@3.12.0' import { ScrollTrigger } from 'https://cdn.skypack.dev/gsap@3.12.0/ScrollTrigger' const CONFIG = { debug: false, // spread: false, // zoom: false, backface: false, // rotate: false, buff: 2, animate: true, scroll: true, dark: true, masklower: 0.9, maskupper: 1.8, perspective: 320, vertical: false, infinite: false, items: 16, gap: 0.1, rotatex: 0, rotatez: 0, } const MAIN = document.querySelector('main') const generateItems = () => { const items = [] const controllers = [] for (let i = 0; i < CONFIG.items + 1; i++) { // scopes.push(`--item-${i}`) if (i !== CONFIG.items) { items.push(`
`) } controllers.push('
') } return { items: items.join(''), controllers: controllers.join(''), } } let scroller const handleScroll = () => { if (!CONFIG.infinite) return false if (CONFIG.vertical) { if (scroller.scrollTop + window.innerHeight > scroller.scrollHeight - 2) { scroller.scrollTop = 2 } if (scroller.scrollTop < 2) { scroller.scrollTop = scroller.scrollHeight - 2 } } else { if (scroller.scrollLeft + window.innerWidth > scroller.scrollWidth - 2) { scroller.scrollLeft = 2 } if (scroller.scrollLeft < 2) { scroller.scrollLeft = scroller.scrollWidth - 2 } } } const setupController = () => { scroller = document.querySelector('.controller') scroller.addEventListener('scroll', handleScroll) } const render = () => { const { controllers, items } = generateItems() MAIN.innerHTML = `
${items}
${controllers}
` setupController() } let tween const update = () => { document.documentElement.dataset.debug = CONFIG.debug // document.documentElement.dataset.spread = CONFIG.spread // document.documentElement.dataset.zoom = CONFIG.zoom // document.documentElement.dataset.rotate = CONFIG.rotate document.documentElement.dataset.animate = CONFIG.animate document.documentElement.dataset.backface = CONFIG.backface document.documentElement.dataset.scroll = CONFIG.scroll document.documentElement.dataset.dark = CONFIG.dark document.documentElement.dataset.vertical = CONFIG.vertical document.documentElement.dataset.infinite = CONFIG.infinite document.documentElement.style.setProperty('--gap-efficient', CONFIG.gap) document.documentElement.style.setProperty('--rotate-x', CONFIG.rotatex) document.documentElement.style.setProperty('--rotate-z', CONFIG.rotatez) document.documentElement.style.setProperty('--mask-lower', CONFIG.masklower) document.documentElement.style.setProperty('--mask-upper', CONFIG.maskupper) document.documentElement.style.setProperty('--scroll-ratio', CONFIG.buff) document.documentElement.style.setProperty( '--perspective', CONFIG.perspective ) // If we're scroll driving, use GSAP if ( !CSS.supports('animation-timeline: scroll()') && CONFIG.scroll && CONFIG.animate ) { // transform: translate3d(0, 0, var(--radius)) rotateX(calc(var(--rotate-x) * 1deg)) rotateZ(calc(var(--rotate-z) * 1deg)) rotateY(-360deg); if (scroller) scroller[CONFIG.vertical ? 'scrollTop' : 'scrollLeft'] = 0 document.documentElement.dataset.gsap = true gsap.registerPlugin(ScrollTrigger) gsap.set(['.carousel'], { animation: 'none', '--rotate': 0 }) tween = gsap.to('.carousel', { rotateY: -360, '--rotate': 360, ease: 'none', scrollTrigger: { horizontal: !CONFIG.vertical, scroller: '.controller', scrub: true, }, }) } else { document.documentElement.dataset.gsap = false gsap.set('.carousel', { clearProps: true }) if (tween) tween.kill() ScrollTrigger.killAll() document.querySelector('.carousel').removeAttribute('style') } } const sync = (event) => { if ( !document.startViewTransition || !event || (event && event.target.controller.view.labelElement.innerText !== 'Dark Theme' && event.target.controller.view.labelElement.innerText !== 'Backface') ) return update() document.startViewTransition(update) } const controlPanel = new Pane({ title: 'Config', expanded: false }) // const spreader = controlPanel.addBinding(CONFIG, 'spread', { label: 'Spread' }) // controlPanel.addBinding(CONFIG, 'zoom', { label: 'Zoom' }) // controlPanel.addBinding(CONFIG, 'rotate', { label: 'Rotate' }) controlPanel.addBinding(CONFIG, 'animate', { label: 'Animate' }) const scrolling = controlPanel.addFolder({ title: 'Scrolling', expanded: false, }) scrolling.addBinding(CONFIG, 'scroll', { label: 'Scroll Drive' }) scrolling.addBinding(CONFIG, 'vertical', { label: 'Vertical' }) scrolling.addBinding(CONFIG, 'infinite', { label: 'Infinite' }) scrolling.addBinding(CONFIG, 'buff', { label: 'Ratio', min: 1, max: 10, step: 0.1, }) scrolling.addBinding(CONFIG, 'debug', { label: 'Debug' }) const rotation = controlPanel.addFolder({ title: 'Rotation', expanded: false }) rotation.addBinding(CONFIG, 'rotatex', { min: 0, max: 360, step: 1, label: 'X', }) rotation.addBinding(CONFIG, 'rotatez', { min: 0, max: 360, step: 1, label: 'Z', }) const masker = controlPanel.addFolder({ title: 'Mask', expanded: false }) masker.addBinding(CONFIG, 'masklower', { label: 'Lower (Item W)', min: 0, max: 5, step: 0.1, }) masker.addBinding(CONFIG, 'maskupper', { label: 'Upper (Item W)', min: 0, max: 5, step: 0.1, }) controlPanel.addBinding(CONFIG, 'backface', { label: 'Backface' }) controlPanel.addBinding(CONFIG, 'perspective', { label: 'Perspective (px)', min: 50, max: 1500, step: 1, }) controlPanel.addBinding(CONFIG, 'gap', { label: 'Gap (%)', min: 0, max: 5, step: 0.1, }) const itemSwitch = controlPanel.addBinding(CONFIG, 'items', { label: 'Items', min: 10, max: 50, step: 1, }) controlPanel.addBinding(CONFIG, 'dark', { label: 'Dark Theme' }) render() sync() controlPanel.on('change', sync) // spreader.on('change', render) itemSwitch.on('change', render) if (!CSS.supports('animation-timeline: scroll()')) { gsap.registerPlugin(ScrollTrigger) }